<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <title>西部车网（重庆） · 智驾标注与可视化制作</title>
  <meta name="description" content="西部车网（重庆）：智驾标注、视频图片处理、动画制作标注等全流程业务支持，1000+ 专业人才保障高质量交付。" />
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="assets/css/base.css" />
  <link rel="stylesheet" href="assets/css/site.css" />
</head>
<body class="page page-home" data-page="home">
  <header class="site-header">
    <nav class="site-nav container">
      <a href="#home" class="site-nav__brand" data-site-brand aria-label="西部车网（重庆）">
        <svg aria-hidden="true" width="28" height="28" viewBox="0 0 24 24" fill="none" data-site-brand-icon>
          <text x="12" y="17" text-anchor="middle" font-size="14" font-weight="700" fill="currentColor">Ju</text>
        </svg>
        <span data-site-brand-text></span>
      </a>
      <ul class="site-nav__links" data-site-nav="desktop">
      </ul> 
      <button id="menuBtn" class="site-nav__toggle" aria-label="打开菜单">
        <svg aria-hidden="true" width="18" height="12" viewBox="0 0 18 12" fill="none">
          <path d="M1 1H17M1 6H17M1 11H17" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
        </svg>
      </button>
    </nav>
    <div id="mobileMenu" class="mobile-menu">
      <div class="container">
        <div class="mobile-menu__list" data-site-nav="mobile">
        </div>
      </div>
    </div>
  </header>

  <main id="home" class="cube-main">
    <div class="cube-main__stage">
      <div id="cube-stage" class="cube-main__canvas">
        <canvas id="three-canvas"></canvas>
      </div>
    </div>
  </main>

  <footer class="icp-footer">
    <span id="icpRecord" data-site-icp>备案号待后台配置</span>
  </footer>

  <script src="https://unpkg.com/three@0.140.0/build/three.min.js"></script>
  <script src="https://unpkg.com/three@0.140.0/examples/js/controls/OrbitControls.js"></script>
  <script src="assets/js/nav.js"></script>
  <script>
    (function() {
      const stage = document.getElementById('cube-stage');
      const canvas = document.getElementById('three-canvas');
      if (!stage || !canvas) return;

      const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.outputEncoding = THREE.sRGBEncoding;

      const scene = new THREE.Scene();
      scene.background = null;

      const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
      camera.position.set(3.4, 2.8, 3.8);

      const controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.06;
      controls.enablePan = false;
      controls.enableZoom = false;
      controls.minDistance = 2.6;
      controls.maxDistance = 7;
      controls.autoRotate = true;
      controls.autoRotateSpeed = 0.32;
      controls.mouseButtons = {
        LEFT: THREE.MOUSE.NONE,
        MIDDLE: THREE.MOUSE.NONE,
        RIGHT: THREE.MOUSE.ROTATE
      };
      controls.touches = {
        ONE: THREE.TOUCH.ROTATE,
        TWO: THREE.TOUCH.NONE
      };
      renderer.domElement.addEventListener('contextmenu', (event) => event.preventDefault());

      scene.add(new THREE.AmbientLight(0xffffff, 0.45));
      const hemiLight = new THREE.HemisphereLight(0x6d8cff, 0x05070d, 0.78);
      scene.add(hemiLight);
      const keyLight = new THREE.DirectionalLight(0xffffff, 1);
      keyLight.position.set(4.5, 5.6, 3.4);
      scene.add(keyLight);
      const rimLight = new THREE.PointLight(0x3760ff, 1.6, 10, 2);
      rimLight.position.set(-4.2, 1.8, -3.2);
      scene.add(rimLight);

      const fallbackFaces = [
        {
          key: 'about',
          label: '关于我们',
          subtitle: '企业概览',
          description: '了解武工工业智研院的发展历程与使命愿景。',
          summary: '国家级智驾数据服务机构。',
          pageRef: 'about',
          route: 'pages/about.html',
          image: null,
          colors: ['#2B63FF', '#1232A4']
        },
        {
          key: 'services',
          label: '业务矩阵',
          subtitle: '智驾数据全流程',
          description: '智驾标注、视频处理、动画标注三大核心业务。',
          summary: '覆盖智驾数据全流程业务能力。',
          pageRef: 'services',
          route: 'pages/services.html',
          image: null,
          colors: ['#224AD9', '#0B1F61']
        },
        {
          key: 'products',
          label: '互动平台',
          subtitle: '数字化工具链',
          description: '自研标注、质检、项目协同的数字化平台体系。',
          summary: '一站式标注与质检管理平台。',
          pageRef: 'products',
          route: 'pages/products.html',
          image: null,
          colors: ['#4E7CFF', '#1936B4']
        },
        {
          key: 'solutions',
          label: '方案案例',
          subtitle: '行业落地实践',
          description: '汽车、文娱等行业项目实践与成果。',
          summary: '跨行业落地经验与方法论。',
          pageRef: 'solutions',
          route: 'pages/solutions.html',
          image: null,
          colors: ['#2461FF', '#0C246E']
        },
        {
          key: 'team',
          label: '专家团队',
          subtitle: '跨学科人才',
          description: '算法、工程、质检、项目管理协同团队。',
          summary: '千人规模的专业团队。',
          pageRef: 'team',
          route: 'pages/team.html',
          image: null,
          colors: ['#1F4CFF', '#0A1D58']
        },
        {
          key: 'contact',
          label: '联系我们',
          subtitle: '项目快速响应',
          description: '获取方案评估、驻场支持与国密本地化部署。',
          summary: '快速联络项目顾问。',
          pageRef: 'contact',
          route: 'pages/contact.html',
          image: null,
          colors: ['#2342B8', '#08143C']
        }
      ];

      async function resolveFaces() {
        try {
          const res = await fetch('/api/content/cube', { cache: 'no-store' });
          if (!res.ok) return fallbackFaces;
          const data = await res.json();
          if (!data || !Array.isArray(data.faces)) return fallbackFaces;
          const map = new Map(data.faces.map((item) => [item.key, item]));
          return fallbackFaces.map((face) => Object.assign({}, face, map.get(face.key) || {}));
        } catch (err) {
          console.warn('[Cube] 未能读取 cube.json，使用默认配置。', err);
          return fallbackFaces;
        }
      }

        function wrapText(text, ctx, maxWidth, maxLines) {
          if (!text) return [];
          const words = String(text).split(/\s+/);
          const lines = [];
          let current = '';
          words.forEach((word) => {
            const candidate = current ? `${current} ${word}` : word;
            if (ctx.measureText(candidate).width <= maxWidth) {
              current = candidate;
            } else {
              if (current) lines.push(current);
              current = word;
            }
          });
          if (current) lines.push(current);
          if (lines.length > maxLines) {
            const trimmed = lines.slice(0, maxLines);
            if (trimmed.length) {
              trimmed[trimmed.length - 1] = `${trimmed[trimmed.length - 1]}…`;
            }
            return trimmed;
          }
          return lines;
        }

        function createLabelTexture(face) {
        const size = 1024;
        const canvas = document.createElement('canvas');
        canvas.width = canvas.height = size;
        const ctx = canvas.getContext('2d');

        const gradient = ctx.createLinearGradient(0, 0, size, size);
        const from = face.colors?.[0] || '#2257FF'; 
        const to = face.colors?.[1] || '#0C1F55'; //魔方边框颜色
        gradient.addColorStop(0, from);
        gradient.addColorStop(1, to);
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, size, size);

        ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
        ctx.lineWidth = 1;
        for (let i = 64; i < size; i += 64) {
          ctx.beginPath(); ctx.moveTo(i, 48); ctx.lineTo(i, size - 48); ctx.stroke();
          ctx.beginPath(); ctx.moveTo(48, i); ctx.lineTo(size - 48, i); ctx.stroke();
        }

        const overlayHeight = face.subtitle || face.description || face.summary ? 320 : 220;
        const overlayTop = size - overlayHeight;
        ctx.fillStyle = 'rgba(8, 12, 30, 0.32)';
        ctx.fillRect(0, overlayTop, size, overlayHeight);

        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.shadowColor = 'rgba(0,0,0,0.35)';
        ctx.shadowBlur = 24;

        const centerX = size / 2;
        const label = String(face.label || '');
        const subtitle = String(face.subtitle || '');
        const detail = String(face.summary || face.description || '');

        let cursorY = overlayTop + 60;
        if (label) {
          ctx.fillStyle = '#ffffff';
          ctx.font = '700 96px Inter, system-ui, sans-serif';
          ctx.fillText(label, centerX, cursorY);
          cursorY += 96 + 24;
        }
        if (subtitle) {
          ctx.fillStyle = 'rgba(255,255,255,0.92)';
          ctx.font = '500 54px Inter, system-ui, sans-serif';
          ctx.fillText(subtitle, centerX, cursorY);
          cursorY += 54 + 20;
        }
        if (detail) {
          ctx.fillStyle = 'rgba(255,255,255,0.82)';
          ctx.font = '400 36px Inter, system-ui, sans-serif';
          const maxWidth = size - 240;
          const lines = wrapText(detail, ctx, maxWidth, 2);
          lines.forEach((line) => {
            ctx.fillText(line, centerX, cursorY);
            cursorY += 44;
          });
        }

        ctx.shadowBlur = 0;

        const texture = new THREE.CanvasTexture(canvas);
        texture.anisotropy = Math.min(12, renderer.capabilities.getMaxAnisotropy());
        texture.generateMipmaps = true;
        texture.encoding = THREE.sRGBEncoding;
        texture.needsUpdate = true;
        return texture;
      }

      function createImageTexture(image, face) {
        const size = 1024;
        const canvas = document.createElement('canvas');
        canvas.width = canvas.height = size;
        const ctx = canvas.getContext('2d');

        ctx.fillStyle = '#0d121f';
        ctx.fillRect(0, 0, size, size);

        const inset = 60;
        const available = size - inset * 2;
        const scale = Math.max(available / image.width, available / image.height);
        const drawWidth = image.width * scale;
        const drawHeight = image.height * scale;
        const offsetX = inset + (available - drawWidth) / 2;
        const offsetY = inset + (available - drawHeight) / 2;

        ctx.save();
        const radius = 48;
        ctx.beginPath();
        ctx.moveTo(inset + radius, inset);
        ctx.lineTo(size - inset - radius, inset);
        ctx.quadraticCurveTo(size - inset, inset, size - inset, inset + radius);
        ctx.lineTo(size - inset, size - inset - radius);
        ctx.quadraticCurveTo(size - inset, size - inset, size - inset - radius, size - inset);
        ctx.lineTo(inset + radius, size - inset);
        ctx.quadraticCurveTo(inset, size - inset, inset, size - inset - radius);
        ctx.lineTo(inset, inset + radius);
        ctx.quadraticCurveTo(inset, inset, inset + radius, inset);
        ctx.closePath();
        ctx.clip();
        ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
        ctx.restore();

        const gradient = ctx.createLinearGradient(0, size - 240, 0, size);
        gradient.addColorStop(0, 'rgba(6, 12, 26, 0)');
        gradient.addColorStop(1, 'rgba(6, 12, 26, 0.82)');
        ctx.fillStyle = gradient;
        ctx.fillRect(inset, size - 240, size - inset * 2, 240);

        ctx.fillStyle = '#ffffff';
        ctx.font = '700 90px Inter, system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.shadowColor = 'rgba(0,0,0,0.45)';
        ctx.shadowBlur = 28;
        ctx.fillText(face.label || '', size / 2, size - 110);

        ctx.lineWidth = 6;
        ctx.strokeStyle = 'rgba(255,255,255,0.18)';
        ctx.strokeRect(inset + 6, inset + 6, size - (inset + 6) * 2, size - (inset + 6) * 2);

        const texture = new THREE.CanvasTexture(canvas);
        texture.anisotropy = Math.min(12, renderer.capabilities.getMaxAnisotropy());
        texture.generateMipmaps = true;
        texture.encoding = THREE.sRGBEncoding;
        texture.needsUpdate = true;
        return texture;
      }

      function buildMaterial(face) {
        return new Promise((resolve) => {
          if (face.image) {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
              const texture = createImageTexture(img, face);
              console.info(`[Cube] 已加载魔方面 ${face.key} 图片`);
              resolve(new THREE.MeshStandardMaterial({
                map: texture,
                metalness: 0.18,
                roughness: 0.5
              }));
            };
            img.onerror = () => {
              console.warn(`[Cube] 图片加载失败，回退到渐变：${face.image}`);
              resolve(new THREE.MeshStandardMaterial({
                map: createLabelTexture(face),
                metalness: 0.18,
                roughness: 0.55
              }));
            };
            const normalized = String(face.image).trim();
            const resolvedSrc =
              /^https?:\/\//i.test(normalized) || normalized.startsWith('//')
                ? normalized
                : `/${normalized.replace(/^\/+/, '')}`;
            img.src = `${resolvedSrc}${resolvedSrc.includes('?') ? '&' : '?'}_=${Date.now()}`;
            return;
          }

          resolve(new THREE.MeshStandardMaterial({
            map: createLabelTexture(face),
            metalness: 0.18,
            roughness: 0.55
          }));
        });
      }

      const pointer = new THREE.Vector2();
      const raycaster = new THREE.Raycaster();
      let activeFaces = [];
      let cube = null;

      async function initCube() {
        activeFaces = await resolveFaces();
        const materials = await Promise.all(activeFaces.map(buildMaterial));

        cube = new THREE.Mesh(new THREE.BoxGeometry(2.9, 2.9, 2.9), materials);
        cube.castShadow = false;
        cube.receiveShadow = false;
        scene.add(cube);

        renderer.domElement.addEventListener('click', handleClick);
        animate();
      }

      function handleClick(event) {
        if (!cube) return;
        const rect = renderer.domElement.getBoundingClientRect();
        pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
        raycaster.setFromCamera(pointer, camera);
        const intersections = raycaster.intersectObject(cube);
        if (!intersections.length) return;
        const faceIndex = intersections[0].faceIndex || 0;
        const face = activeFaces[Math.floor(faceIndex / 2)];
        if (!face) return;

        triggerSelectionAnimation();

        if (face.route) {
          setTimeout(() => {
            window.location.href = face.route;
          }, 240);
        }
      }

      function triggerSelectionAnimation() {
        if (!cube) return;
        const start = performance.now();
        const duration = 160;
        const initial = 1;
        const target = 0.94;

        function easeInOut(t) {
          return 0.5 * (1 - Math.cos(Math.PI * t));
        }

        function step(now) {
          const elapsed = Math.min((now - start) / duration, 1);
          const scale = initial + (target - initial) * easeInOut(elapsed);
          cube.scale.setScalar(scale);
          if (elapsed < 1) {
            requestAnimationFrame(step);
          } else {
            const bounceStart = performance.now();
            const bounceDuration = 180;
            function bounce(now2) {
              const t = Math.min((now2 - bounceStart) / bounceDuration, 1);
              const eased = 1 - Math.pow(1 - t, 3);
              const bounceScale = target + (1.02 - target) * eased;
              cube.scale.setScalar(bounceScale);
              if (t < 1) {
                requestAnimationFrame(bounce);
              } else {
                cube.scale.setScalar(1);
              }
            }
            requestAnimationFrame(bounce);
          }
        }

        requestAnimationFrame(step);
      }

      function resize() {
        let { clientWidth: width, clientHeight: height } = stage;

        if (!height || height === 0) {
          if (width && width > 0) {
            height = width;
          } else {
            const fallback = Math.round(window.innerHeight * (window.innerWidth >= 1024 ? 0.6 : 0.7));
            height = fallback;
            width = fallback;
          }
        }

        if (!width || width === 0) {
          width = height || stage.offsetWidth || document.documentElement.clientWidth || 800;
        }

        renderer.setSize(width, height, false);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
      }

      function animate() {
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(animate);
      }

      resize();
      window.addEventListener('resize', resize);

      let resumeTimer = null;
      controls.addEventListener('start', () => {
        controls.autoRotate = false;
        if (resumeTimer) {
          clearTimeout(resumeTimer);
          resumeTimer = null;
        }
      });
      controls.addEventListener('end', () => {
        resumeTimer = setTimeout(() => {
          controls.autoRotate = true;
        }, 1800);
      });

      initCube();
    })();
  </script>
</body>
</html>
